/*******************************************************************************
 * Copyright (c) 2006, 2016 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *     Jonah Graham (Kichwa Coders) - Bug 317173 - cleanup warnings
 *******************************************************************************/
package org.eclipse.cdt.dsf.service;

import java.util.Arrays;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.cdt.dsf.concurrent.DsfExecutor;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceRegistration;


/**
 * Standard base implementation of the DSF service.  This is a convenience
 * class that provides the basic functionality that all DSF services have 
 * to implement.
 * 
 * @since 1.0
 */
abstract public class AbstractDsfService 
    implements IDsfService, IDsfStatusConstants
{
    /** Reference to the session that this service belongs to. */ 
    private DsfSession fSession;

    /** Startup order number of this service. */
    private int fStartupNumber;
    
    /** Registration object for this service. */
    private ServiceRegistration<?> fRegistration;
    
    /** Tracker for services that this service depends on. */
    private DsfServicesTracker fTracker;
    
    /** Properties that this service was registered with */
    @SuppressWarnings("rawtypes")
    private Dictionary fProperties;

    /** Properties that this service was registered with */
    private String fFilter;

    /** 
     * Only constructor, requires a reference to the session that this
     * service  belongs to.
     * @param session
     */
    public AbstractDsfService(DsfSession session) {
        fSession = session;
    }

    @Override
    public DsfExecutor getExecutor() { return fSession.getExecutor(); }

	/**
	 * The the returned collection is a superset of the properties specified in
	 * {@link #register(String[], Dictionary)}. That method can add additional
	 * (implicit) properties. For one, it tacks on the
	 * {@link Constants#OBJECTCLASS} property associated with the service after
	 * it has been registered. It also adds a property that designates the dsf
	 * session ID.
	 * 
	 * @see org.eclipse.cdt.dsf.service.IDsfService#getProperties()
	 */
    @SuppressWarnings("rawtypes")
    @Override
    public Dictionary getProperties() { return fProperties; }
    
    @Override
    public String getServiceFilter() { return fFilter; }
    
    @Override
    public int getStartupNumber() { return fStartupNumber; }
    
    @Override
    public void initialize(RequestMonitor rm) {
        fTracker = new DsfServicesTracker(getBundleContext(), fSession.getId());
        fStartupNumber = fSession.getAndIncrementServiceStartupCounter();
        rm.done();
    }
        
    @Override
    public void shutdown(RequestMonitor rm) {
        fTracker.dispose();
        fTracker = null;
        rm.done();
    }
    
    /**
	 * @since 2.0
	 */
    @Override
    public boolean isRegistered() {
        return getServiceRegistration() != null;
    }

    /** Returns the session object for this service */
    @Override
    public DsfSession getSession() { return fSession; }

    /**
     * Sub-classes should return the bundle context of the plugin, which the 
     * service belongs to.
     */
    abstract protected BundleContext getBundleContext();
    
    /**  Returns the tracker for the services that this service depends on. */
    protected DsfServicesTracker getServicesTracker() { return fTracker; }

	/**
	 * Registers this DSF/OSGi service.
	 * 
	 * @param classes
	 *            The class names under which the service can be located. For
	 *            convenience, [classes] need not contain {@link IDsfService} or
	 *            the runtime class of this object; they are automatically added
	 *            if not present. The complete list of classes the service ends
	 *            up being registered in OSGi with is recorded and made
	 *            available via
	 *            <code>getProperties().get(Constants.OBJECTCLASS)</code>
	 * @param properties
	 *            The properties for this service. The keys in the properties
	 *            object must all be <code>String</code> objects. See
	 *            {@link Constants} for a list of standard service property
	 *            keys. Changes should not be made to this object after calling
	 *            this method. To update the service's properties the
	 *            {@link ServiceRegistration#setProperties} method must be
	 *            called. Caller should, at a minimum, pass an empty
	 *            dictionary--never null. We add a property to the collection
	 *            (we modify the caller's object), to record the id of the dsf
	 *            session associated with the service.
	 */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    protected void register(String[] classes, Dictionary properties) {
    	
    	/*
    	 * If this service has already been registered, make sure we
    	 * keep the names it has been registered with.  However, we
    	 * must trigger a new registration or else OSGI will keep the two
    	 * registration separate. 
    	 */
    	if (fRegistration != null) {
    		String[] previousClasses = (String[])fRegistration.getReference().getProperty(Constants.OBJECTCLASS);
    		
            // Use a HashSet to avoid duplicates
        	Set<String> newClasses = new HashSet<String>();
        	newClasses.addAll(Arrays.asList(previousClasses));
        	newClasses.addAll(Arrays.asList(classes));
        	classes = newClasses.toArray(new String[0]);

        	/*
        	 * Also keep all previous properties.
        	 */
            if (fProperties != null) {
            	for (Enumeration e = fProperties.keys() ; e.hasMoreElements();) {
            		Object key = e.nextElement();
            		Object value = fProperties.get(key);
            		properties.put(key, value);
            	}
            }
            
        	// Now, cancel the previous registration
    		unregister();
    	}
        /*
         * Ensure that the list of classes contains the base DSF service 
         * interface, as well as the actual class type of this object.
         */
        if (!Arrays.asList(classes).contains(IDsfService.class.getName())) {
            String[] newClasses = new String[classes.length + 1];
            System.arraycopy(classes, 0, newClasses, 1, classes.length);
            newClasses[0] = IDsfService.class.getName();
            classes = newClasses;
        }
        if (!Arrays.asList(classes).contains(getClass().getName())) {
            String[] newClasses = new String[classes.length + 1];
            System.arraycopy(classes, 0, newClasses, 1, classes.length);
            newClasses[0] = getClass().getName();
            classes = newClasses;
        }
        /*
         * Make sure that the session ID is set in the service properties.
         * The session ID distinguishes this service instance from instances
         * of the same service in other sessions.
         */
        properties.put(PROP_SESSION_ID, getSession().getId());
        fProperties = properties;
        fRegistration = getBundleContext().registerService(classes, this, properties);
        
        /*
         * Retrieve the OBJECTCLASS property directly from the service 
         * registration info.  This is the best bet for getting an accurate 
         * value.
         */
        fProperties.put(Constants.OBJECTCLASS, fRegistration.getReference().getProperty(Constants.OBJECTCLASS));
        
        /*
         * Create the filter for this service based on all the properties.  If 
         * there is a single service instance per session, or if the properties
         * parameter uniquely identifies this service instance among other 
         * instances in this session.  Then this filter will fetch this service
         * and only this service from OSGi.
         */
        fFilter = generateFilter(fProperties);
    }

    /**
     * Generates an LDAP filter to uniquely identify this service.
     */
    @SuppressWarnings({ "rawtypes" })
    private String generateFilter(Dictionary properties) {
        StringBuilder filter = new StringBuilder();
        filter.append("(&"); //$NON-NLS-1$
        
        for (Enumeration keys = properties.keys(); keys.hasMoreElements();) {
            Object key = keys.nextElement();
            Object value = properties.get(key);
            if (value instanceof Object[]) {
                /*
                 * For arrays, add a test to check that every element in array 
                 * is present.  This is here mainly to handle OBJECTCLASS property.
                 */
                for (Object arrayValue : (Object[])value) {
                    filter.append('(');
                    filter.append(key.toString());
                    filter.append("=*"); //$NON-NLS-1$
                    filter.append(arrayValue.toString());
                    filter.append(')');
                }
            } else {
                filter.append('(');
                filter.append(key.toString());
                filter.append('=');
                filter.append(value.toString());
                filter.append(')');
            }
        }
        filter.append(')');
        return filter.toString();
    }
    
    /** 
     * De-registers this service.
     *
     */
    protected void unregister() {
    	if (fRegistration != null) {
    		fRegistration.unregister();
    	}
    	fRegistration = null;
    }

    /** Returns the registration object that was obtained when this service was registered */
    protected ServiceRegistration<?> getServiceRegistration() { return fRegistration; }
}
